<?php


namespace coco\client;


use coco\Config;
use RedisException;

/**
 * 重连redis 方法同原生redis扩展
 * Class Redis
 * @package coco\client
 * @property-read bool $connected
 * @method bool connect(string $host, int $port, float $timeout, null $reserved, int $retryInterval, float $readTimeout)
 * @method bool isConnected()
 * @method string|false getHost()
 * @method int|false getPort()
 * @method int|bool getDbNum()
 * @method float|false getTimeout()
 * @method float|bool getReadTimeout()
 * @method string|null|bool getPersistentID()
 * @method string|null|bool getAuth()
 * @method bool pconnect(string $host, int $port, float $timeout, string $persistentId, int $retryInterval, float $readTimeout)
 * @method bool close()
 * @method bool swapdb(int $db1, int $db2)
 * @method bool setOption(int $option, mixed $value)
 * @method mixed|null getOption(int $option)
 * @method bool|string ping(string $message)
 * @method string echo (string $message)
 * @method string|false get(string $key)
 * @method bool set(string $key, string|mixed $value, int|array $timeout = null)
 * @method bool setex(string $key, int $ttl, string|mixed $value)
 * @method bool psetex(string $key, int $ttl, string|mixed $value)
 * @method bool setnx(string $key, string|mixed $value)
 * @method int del(int|string|array $key1, int|string ...$otherKeys)
 * @method int unlink(string|string[] $key1, string $key2, string $key3)
 * @method self multi(int $mode)
 * @method self pipeline()
 * @method void|array exec()
 * @method discard()
 * @method void watch(string|string[] $key)
 * @method unwatch()
 * @method mixed|null subscribe(string[] $channels, string|array $callback)
 * @method psubscribe(array $patterns, string|array $callback)
 * @method int publish(string $channel, string $message)
 * @method array|int pubsub(string $keyword, string|array $argument)
 * @method unsubscribe(array $channels)
 * @method punsubscribe(array $patterns)
 * @method int|bool exists(string|string[] $key)
 * @method int incr(string $key)
 * @method float incrByFloat(string $key, float $increment)
 * @method int incrBy(string $key, int $value)
 * @method int decr(string $key)
 * @method int decrBy(string $key, int $value)
 * @method int|false lPush(string $key, string|mixed ...$value1)
 * @method int|false rPush(string $key, string|mixed ...$value1)
 * @method int|false lPushx(string $key, string|mixed $value)
 * @method int|false rPushx(string $key, string|mixed $value)
 * @method lPop(string $key)
 * @method rPop(string $key)
 * @method array blPop(string|string[] $keys, int $timeout)
 * @method array brPop(string|string[] $keys, int $timeout)
 * @method int|bool lLen(string $key)
 * @method mixed|bool lIndex(string $key, int $index)
 * @method bool lSet(string $key, int $index, string $value)
 * @method array lRange(string $key, int $start, int $end)
 * @method array|false lTrim(string $key, int $start, int $stop)
 * @method int|bool lRem(string $key, string $value, int $count)
 * @method int lInsert(string $key, int $position, string $pivot, string|mixed $value)
 * @method int|bool sAdd(string $key, string|mixed ...$value1)
 * @method int sRem(string $key, string|mixed ...$member1)
 * @method bool sMove(string $srcKey, string $dstKey, string|mixed $member)
 * @method bool sIsMember(string $key, string|mixed $value)
 * @method int sCard(string $key)
 * @method string|mixed|array|bool sPop(string $key, int $count)
 * @method string|mixed|array|bool sRandMember(string $key, int $count)
 * @method array sInter(string $key1, string ...$otherKeys)
 * @method int|false sInterStore(string $dstKey, string $key1, string ...$otherKeys)
 * @method array sUnion(string $key1, string ...$otherKeys)
 * @method int sUnionStore(string $dstKey, string $key1, string ...$otherKeys)
 * @method array sDiff(string $key1, string ...$otherKeys)
 * @method int|false sDiffStore(string $dstKey, string $key1, string ...$otherKeys)
 * @method array sMembers(string $key)
 * @method array|false sScan(string $key, int &$iterator, string $pattern, int $count)
 * @method string|mixed getSet(string $key, string|mixed $value)
 * @method string randomKey()
 * @method bool select(int $dbIndex)
 * @method bool move(string $key, int $dbIndex)
 * @method bool rename(string $srcKey, string $dstKey)
 * @method bool renameNx(string $srcKey, string $dstKey)
 * @method bool expire(string $key, int $ttl)
 * @method bool pExpire(string $key, int $ttl)
 * @method bool expireAt(string $key, int $timestamp)
 * @method bool pExpireAt(string $key, int $timestamp)
 * @method array keys(string $pattern)
 * @method int dbSize()
 * @method bool auth(string $password)
 * @method bool bgrewriteaof()
 * @method bool slaveof(string $host, int $port)
 * @method mixed slowLog(string $operation, int|null $length)
 * @method string|int|false object(string $string, string $key)
 * @method bool save()
 * @method bool bgsave()
 * @method int lastSave()
 * @method wait(int $numSlaves, int $timeout)
 * @method int type(string $key)
 * @method int append(string $key, string|mixed $value)
 * @method string getRange(string $key, int $start, int $end)
 * @method int setRange(string $key, int $offset, string $value)
 * @method int strlen(string $key)
 * @method int bitpos(string $key, int $bit, int $start, int $end)
 * @method int getBit(string $key, int $offset)
 * @method int setBit(string $key, int $offset, bool|int $value)
 * @method int bitCount(string $key)
 * @method int bitOp(string $operation, string $retKey, string $key1, string ...$otherKeys)
 * @method bool flushDB()
 * @method bool flushAll()
 * @method array sort(string $key, array $option)
 * @method string info(string $option)
 * @method bool resetStat()
 * @method int|bool ttl(string $key)
 * @method int|bool pttl(string $key)
 * @method bool persist(string $key)
 * @method bool mset(array $array)
 * @method array mget(array $array)
 * @method int msetnx(array $array)
 * @method string|mixed|false rpoplpush(string $srcKey, string $dstKey)
 * @method brpoplpush(string $srcKey, string $dstKey, int $timeout)
 * @method int zAdd(string $key, float $score1, string $value1)
 * @method array zRange(string $key, int $start, int $end, bool $withscores = false)
 * @method int zRem(string $key, string|mixed $member1, string|mixed ...$otherMembers)
 * @method array zRevRange(string $key, int $start, int $end, bool $withscore)
 * @method array zRangeByScore(string $key, int $start, int $end, array $options = [])
 * @method array zRevRangeByScore(string $key, int $start, int $end, array $options)
 * @method array|bool zRangeByLex(string $key, int $min, int $max, int $offset, int $limit)
 * @method array zRevRangeByLex(string $key, int $min, int $max, int $offset, int $limit)
 * @method int zCount(string $key, string $start, string $end)
 * @method int zRemRangeByScore(string $key, float|string $start, float|string $end)
 * @method int zRemRangeByRank(string $key, int $start, int $end)
 * @method int zCard(string $key)
 * @method float|bool zScore(string $key, string|mixed $member)
 * @method int|false zRank(string $key, string|mixed $member)
 * @method int|false zRevRank(string $key, string|mixed $member)
 * @method float zIncrBy(string $key, float $value, string $member)
 * @method int zUnionStore(string $output, array $zSetKeys, array $weights, string $aggregateFunction)
 * @method int zInterStore(string $output, array $zSetKeys, array $weights, string $aggregateFunction)
 * @method array|false zScan(string $key, int &$iterator, string $pattern, int $count)
 * @method array bzPopMax(string|array $key1, string|array $key2, int $timeout)
 * @method array bzPopMin(string|array $key1, string|array $key2, int $timeout)
 * @method array zPopMax(string $key, int $count)
 * @method array zPopMin(string $key, int $count)
 * @method int|bool hSet(string $key, string $hashKey, string $value)
 * @method hSetNx(string $key, string $hashKey, string $value)
 * @method string|false hGet(string $key, string $hashKey)
 * @method int|false hLen(string $key)
 * @method int|bool hDel(string $key, string $hashKey1, string ...$otherHashKeys)
 * @method array hKeys(string $key)
 * @method array hVals(string $key)
 * @method array hGetAll(string $key)
 * @method bool hExists(string $key, string $hashKey)
 * @method int hIncrBy(string $key, string $hashKey, int $value)
 * @method float hIncrByFloat(string $key, string $field, float $increment)
 * @method bool hMSet(string $key, array $hashKeys)
 * @method array hMGet(string $key, array $hashKeys)
 * @method array hScan(string $key, int &$iterator, string $pattern, int $count)
 * @method int hStrLen(string $key, string $field)
 * @method int geoadd(string $key, float $longitude, float $latitude, string $member)
 * @method array geohash(string $key, string ...$member)
 * @method array geopos(string $key, string $member)
 * @method float geodist(string $key, string $member1, string $member2, string|null $unit)
 * @method mixed georadius($key, $longitude, $latitude, $radius, $unit, array|null $options)
 * @method array georadiusbymember(string $key, string $member, $radius, $units, array|null $options)
 * @method array config(string $operation, string $key, string|mixed $value = '')
 * @method mixed eval(string $script, array $args, int $numKeys)
 * @method mixed evalSha(string $scriptSha, array $args, int $numKeys)
 * @method script(string $command, string $script)
 * @method string|null getLastError()
 * @method bool clearLastError()
 * @method mixed client(string $command, string $value)
 * @method string _prefix(mixed $value)
 * @method mixed _unserialize(string $value)
 * @method _serialize(mixed $value)
 * @method string|false dump(string $key)
 * @method bool restore(string $key, int $ttl, string $value)
 * @method bool migrate(string $host, int $port, string $key, int $db, int $timeout, bool $copy, bool $replace)
 * @method array time()
 * @method array|false scan(int &$iterator, string $pattern, int $count)
 * @method bool pfAdd(string $key, array $elements)
 * @method int pfCount(string|array $key)
 * @method bool pfMerge(string $destKey, array $sourceKeys)
 * @method mixed rawCommand(string $command, mixed $arguments)
 * @method int getMode()
 * @method int xAck(string $stream, string $group, array $messages)
 * @method string xAdd(string $key, string $id, array $messages, int $maxLen, bool $isApproximate)
 * @method array xClaim(string $key, string $group, string $consumer, int $minIdleTime, array $ids, array $options)
 * @method int xDel(string $key, array $ids)
 * @method mixed xGroup(string $operation, string $key, string $group, string $msgId, bool $mkStream)
 * @method mixed xInfo(string $operation, string $stream, string $group)
 * @method int xLen(string $stream)
 * @method array xPending(string $stream, string $group, string $start, string $end, int $count, string $consumer)
 * @method array xRange(string $stream, string $start, string $end, int $count)
 * @method array xRead(array $streams, int|string $count, int|string $block)
 * @method array xReadGroup(string $group, string $consumer, array $streams, int|null $count, int|null $block)
 * @method array xRevRange(string $stream, string $end, string $start, int $count)
 * @method int xTrim(string $stream, int $maxLen, bool $isApproximate)
 * @method sAddArray(string $key, array $values)
 */
class Redis
{
    protected $connected = false;
    private $handle;
    private $host;
    private $option;
    private $reconnect_times = 0;
    private $connect_time = 0;

    /**
     * Redis constructor.
     * @param string $host
     * @param array $option
     */
    public function __construct(string $host = '', array $option = [])
    {
        $this->host = $host;
        $this->option = $option;
    }

    public function __get($name)
    {
        if (property_exists($this, $name)) {
            return $this->$name;
        }
        return null;
    }

    /**
     * @param $name
     * @param $arguments
     * @return false|mixed
     * @throws RedisException
     */
    public function __call($name, $arguments)
    {
        $config = Config::getInstance()->get('redis');
        $keep_cd = $this->option['keep_cd'] ?? $config['keep_cd'] ?? 60;
        if ($this->connect_time < time() - $keep_cd) {
            $this->keep();
        }
        if ($this->connected) {
            if (method_exists($this->handle, $name)) {
                $return = call_user_func_array([$this->handle, $name], $arguments);
                $this->connect_time = time();
                return $return;
            }
        }
    }

    protected function keep()
    {
        if ($this->connected === false) {
            $this->reconnect();
            return;
        }
        try {
            $this->handle->ping('ping');
            $this->connect_time = time();
        } catch (RedisException $e) {
            $this->connected = false;
            $this->reconnect();
        }
    }

    /**
     * @return bool
     */
    final protected function reconnect()
    {
        if ($this->handle instanceof \Redis) {
            $this->handle->close();
            $this->handle = null;
        }
        $this->handle = new \Redis();
        $config = Config::getInstance()->get('redis');
        $host = $this->host ?: $config['host'] ?? '127.0.0.1';
        $port = $this->option['port'] ?? $config['port'] ?? 6379;
        $password = $this->option['password'] ?? $config['password'] ?? '';
        $db = $this->option['db'] ?? $config['db'] ?? 0;
        $reconnect_times = $this->option['reconnect'] ?? $config['reconnect'] ?? 10;
        if ($this->reconnect_times++ > $reconnect_times) {
            throw new RedisException('fail:重连次数过多');
        }
        if (true === $this->handle->connect($host, $port)) {
            if (!empty($password)) {
                $this->handle->auth($password);
            }
            if (!empty($db)) {
                $this->handle->select((int)$db);
            }
            $this->connected = true;
        }
        $this->keep();
    }
}